package com.jj.message;

import com.google.protobuf.GeneratedMessageV3;
import com.jj.battle.protocol.GameProto;
import com.jj.battle.protocol.ProtocolId;
import com.jj.core.utils.Hex;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

@Data
@Component
public class PBMessageFactory {
    private static Logger logger = LoggerFactory.getLogger(PBMessageFactory.class);

    public static final String clientClsPrefix = "com.jj.battle.protocol.GameProto";

    Map<Integer, Method> methodDict = new HashMap<>();
    Map<String, Method> writeToMethodDict = new HashMap<>();
    Map<Class<?>, Integer> messageIdx = new HashMap<>();

    Map<Integer, String> msgNames = new HashMap<>();

    @PostConstruct
    public void init(){

        //register user protocol
        for(ProtocolId.ProtoId id : ProtocolId.ProtoId.values()){
            try{
                regProtocol(id, clientClsPrefix);
                msgNames.put(id.getNumber(), id.name());
            }catch (Throwable t){
                logger.error("failed to reg {}", id);
            }
        };
    }

    public String getMsgName(int msgId){
        return msgNames.get(msgId);
    }

    public String getMsgName(short msgId){
        return msgNames.get((int)msgId);
    }

    @Override
    public String toString() {
        return "PBMessageFactory{" +
                "methodDict=" + methodDict +
                ", writeToMethodDict=" + writeToMethodDict +
                ", messageIdx=" + messageIdx +
                '}';
    }

    public static Object getValue(Object obj, String methodName){
        try {
            Method method = obj.getClass().getMethod(methodName);
            return method.invoke(obj, null);
        } catch (Throwable e) {
            //logger.error("exception", e);
            logger.warn("ignore {} {}", obj, methodName);
            return null;
        }
    }

    /**
     * 返回消息的Index（ID）
     *
     * @param msg
     * @return
     */
    public short getMsgIdx(Object msg){
        if(msg instanceof GeneratedMessageV3)
        {
            return messageIdx.get(msg.getClass()).shortValue();
        }else{
            logger.error("unknown msg {}", msg);
            if(msg != null){
                logger.error("msg type:{}", msg.getClass());
            }
            return -1;
        }
    }

    /**
     * 根据消息序列号和消息payload，构造出PB消息对象
     * @param msgIdx
     * @param byteArray
     * @return
     */
    public GeneratedMessageV3 build(int msgIdx, byte[] byteArray){
        if(byteArray == null){
            return null;
        }

        try {
            Method method = methodDict.get(msgIdx);
            if (method != null) {
                try {
                    GeneratedMessageV3 msgObj = (GeneratedMessageV3) method.invoke(null, byteArray);
                    return msgObj;
                } catch (IllegalAccessException e) {
                    logger.error("exception", e);
                } catch (InvocationTargetException e) {
                    logger.error("exception", e);
                }
            } else {
                logger.error("msgIdx {} not found", msgIdx);
            }
        }catch(Throwable t){
            logger.error("exception", t);
        }
        return null;
    }

    public GeneratedMessageV3 build(short msgIdx, byte[] byteArray){
        return build((int)msgIdx, byteArray);
    }

    public void writeTo(GeneratedMessageV3 pbMsg, ByteBuf byteBuf){
        ByteBufOutputStream outputStream = new ByteBufOutputStream(byteBuf);
        Method writeToMethod = writeToMethodDict.get(pbMsg.getClass().getName());
        try {
            writeToMethod.invoke(pbMsg, outputStream);
        } catch (IllegalAccessException e) {
            logger.error("exception", e);
        } catch (InvocationTargetException e) {
            logger.error("exception", e);
        }
    }

    public byte[] serialize(GeneratedMessageV3 pbMsg){
        byte[] rawData = null;
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(pbMsg.getSerializedSize());
        Method writeToMethod = writeToMethodDict.get(pbMsg.getClass().getName());
        try {
            writeToMethod.invoke(pbMsg, outputStream);
            rawData = outputStream.toByteArray();
        } catch (IllegalAccessException e) {
            logger.error("exception", e);
        } catch (InvocationTargetException e) {
            logger.error("exception", e);
        }finally {
            return rawData;
        }
    }

    public Class<?> regProtocol(Object protoId,
                                       String clsPrefix
                                        ){
        logger.debug("name:{}", protoId.getClass().getName());
        String enumName = (String) getValue(protoId, "name");
        int number = (int)getValue(protoId, "getNumber");
        String realClassName = clsPrefix + "$" + enumName.replaceFirst("MSG", "PKG");
        Class<?> cls = null;
        logger.debug("{} -> {}",  enumName, realClassName);
        try {
            //reg id --> class
            cls = Class.forName(realClassName);
            //logger.debug("find cls:{}", cls);
            Method method = cls.getMethod("parseFrom", byte[].class);
            if(method != null){
                //logger.debug("find method:{}", method);
                methodDict.put(number, method);
                logger.info("register {} -> {} , cls:{}",
                        number, enumName, cls.getName());

            }else{
                logger.warn("writeTo method not found for {}", enumName);
            }
            if(cls == null){
                logger.warn("clas not found for {}, ignore", realClassName);
            }

            //reg serialize method
            try{
                Method writeToMethod = cls.getMethod("writeTo", OutputStream.class);
                if(writeToMethod != null) {
                    writeToMethodDict.put(realClassName, writeToMethod);
                }else{
                    logger.warn("writeTo method not found for {}", enumName);
                }
            }catch (Throwable t){
                logger.error("exception", t);
            }

            //register class --> id
            messageIdx.put(cls, number);

        } catch (Throwable e) {
            logger.error("{} can not find right class", realClassName);
        }
        return cls;
    }

    public static void main(String[] args) {
        PBMessageFactory factory = new PBMessageFactory();
        factory.init();
        GameProto.PKG_SYN_STATE_REQ req = null;
        GameProto.PKG_SYN_STATE_REQ.Builder builder = GameProto.PKG_SYN_STATE_REQ.newBuilder();
        builder.setFrameNo(999);
        req = builder.build();
        byte[] rawData = factory.serialize(req);
        logger.debug("index:{}", factory.getMsgIdx(req));
        Object msg = factory.build(factory.getMsgIdx(req), rawData);
        logger.debug("indexs:{}", factory.getMessageIdx());
        logger.debug("{}", Hex.printhex2Buf(rawData));
        logger.debug("msg:{}", msg);

    }
}
